feat(wallet)!: wire GasFeeController into default initialization#9527
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@metamaskbot publish-previews |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
Construct the extension's GasFeeController through @metamask/wallet instead of the local messenger-client-init wiring, integrating MetaMask/core#9527. The wallet now owns GasFeeController as an instanceOptions.gasFeeController slot: it builds getProvider and getCurrentNetworkEIP1559Compatibility from NetworkController itself and lets the controller default onNetworkDidChange and getChainId via its messenger subscription. The extension supplies only the client-specific overrides (interval, clientId, gas API endpoints, and the BSC legacy gas API compatibility check). - Add wallet-init/instance-options/gas-fee-controller.ts and a minimal wallet-init/messengers/gas-fee-controller-messenger.ts (delegates only NetworkController:getState + getNetworkClientById for the chain-id read). - Wire gasFeeController into wallet-init/initialization.ts. - Resolve this.gasFeeController via this.wallet.getInstance('GasFeeController') and drop the GasFeeControllerInit import + init-map entry. - Delete the local confirmations/gas-fee-controller-init and messengers/gas-fee-controller-messenger (+tests) and their entries in messenger-client-init/messengers/index.ts. GasFeeController stays in controller-list.ts. Preview-pin @metamask/wallet and @metamask/gas-fee-controller to the core#9527 preview builds (7ed2770ae) until a wallet release ships it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Construct the extension's GasFeeController through @metamask/wallet instead of the local messenger-client-init wiring, integrating MetaMask/core#9527. The wallet now owns GasFeeController as an instanceOptions.gasFeeController slot: it builds getProvider and getCurrentNetworkEIP1559Compatibility from NetworkController itself and lets the controller default onNetworkDidChange and getChainId via its messenger subscription. The extension supplies only the client-specific overrides (interval, clientId, gas API endpoints, and the BSC legacy gas API compatibility check). - Add wallet-init/instance-options/gas-fee-controller.ts and a minimal wallet-init/messengers/gas-fee-controller-messenger.ts (delegates only NetworkController:getState + getNetworkClientById for the chain-id read). - Wire gasFeeController into wallet-init/initialization.ts. - Resolve this.gasFeeController via this.wallet.getInstance('GasFeeController') and drop the GasFeeControllerInit import + init-map entry. - Delete the local confirmations/gas-fee-controller-init and messengers/gas-fee-controller-messenger (+tests) and their entries in messenger-client-init/messengers/index.ts. GasFeeController stays in controller-list.ts. Preview-pin @metamask/wallet and @metamask/gas-fee-controller to the core#9527 preview builds (7ed2770ae) until a wallet release ships it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rekmarks
left a comment
There was a problem hiding this comment.
Looks good! Just a couple of things.
Construct the extension's GasFeeController through @metamask/wallet instead of the local messenger-client-init wiring, integrating MetaMask/core#9527. The wallet now owns GasFeeController as an instanceOptions.gasFeeController slot: it builds getProvider and getCurrentNetworkEIP1559Compatibility from NetworkController itself and lets the controller default onNetworkDidChange and getChainId via its messenger subscription. The extension supplies only the client-specific overrides (interval, clientId, gas API endpoints, and the BSC legacy gas API compatibility check). - Add wallet-init/instance-options/gas-fee-controller.ts and a minimal wallet-init/messengers/gas-fee-controller-messenger.ts (delegates only NetworkController:getState + getNetworkClientById for the chain-id read). - Wire gasFeeController into wallet-init/initialization.ts. - Resolve this.gasFeeController via this.wallet.getInstance('GasFeeController') and drop the GasFeeControllerInit import + init-map entry. - Delete the local confirmations/gas-fee-controller-init and messengers/gas-fee-controller-messenger (+tests) and their entries in messenger-client-init/messengers/index.ts. GasFeeController stays in controller-list.ts. Preview-pin @metamask/wallet and @metamask/gas-fee-controller to the core#9527 preview builds (7ed2770ae) until a wallet release ships it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review feedback on #9527: - Make `instanceOptions.gasFeeController.clientId` required and drop the `'cli'` default, so every client (extension, mobile, wallet-cli) must identify itself to the gas API rather than silently inheriting a default. `gasFeeController` is now a required instance-options key (mirroring `networkController`); wallet-cli passes `clientId: 'cli'` explicitly. - Throw a descriptive error naming the members of the cycle when `orderByDependencies` cannot satisfy the declared dependencies, instead of silently deferring to a later "handler not registered" messenger error. Also re-add the `gas-fee-controller` instance rule via the new programmatic `codeowners.ts` generator (#9529) picked up in the rebase onto main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7ed2770 to
ec8eeec
Compare
| * @returns The configurations ordered so that dependencies come first. | ||
| * @throws If the configurations contain a dependency cycle. | ||
| */ | ||
| export function orderByDependencies( |
There was a problem hiding this comment.
This is technically very cool! But logistically terrifying 😅
Should we ensure we don't migrate packages until they are initialisation-order-agnostic?
We had to do this for TransactionController for example.
I'd hope these dependencies are very rare, as we usually rely on legacy callbacks or dynamic messenger calls, rather than hard dependencies at constructor / initialisation time?
There was a problem hiding this comment.
Should we ensure we don't migrate packages until they are initialisation-order-agnostic?
We'll look into what it'd take to do this, but are we not already doing the equivalent of this in the clients?
There was a problem hiding this comment.
@matthewwalsh0 here is the controller change #9569
My review was addressed, holding approval pending resolution of other feedback.
…gnostic (MetaMask#9569) ## Explanation The `GasFeeController` constructor was the only currently-wired-or-wireable controller that performed **eager, construction-time reads** of `NetworkController` and the network provider: - `new EthQuery(this.#getProvider())` — unconditional. - Seeding `currentChainId` — either via an eager `getChainId()` call or via `NetworkController:getState` + `getNetworkClientById`. This forced any shared initialization (e.g. `@metamask/wallet`) to guarantee `NetworkController` is constructed *before* `GasFeeController`, which is exactly the construction-order coupling we want to avoid. Every other order-agnostic controller resolves its collaborators lazily. This PR defers those reads to first use so the controller can be constructed in any order, matching the pattern already used elsewhere. **No public API change** — the constructor signature and gas-fee-fetching behavior are unchanged. ## Changes - Removed the eager `EthQuery` build and the eager `currentChainId` seeding from the constructor. Network-change subscription wiring is kept as-is (subscribing is safe regardless of construction order). - Added lazy, memoized resolution: - `#getEthQuery()` — builds the `EthQuery` from `getProvider` on first use. - `#getCurrentChainId()` — resolves the chain ID from the `getChainId` callback when provided (only when paired with `onNetworkDidChange`, preserving the original coupling), otherwise from `NetworkController`. - `#onNetworkControllerDidChange` now resets `ethQuery` to `undefined` (rebuilt lazily on the next fetch) instead of rebuilding it eagerly per network change, and still updates `currentChainId`. ## Testing - New tests assert construction performs **no** `NetworkController:getState` / `getNetworkClientById` / `getProvider` / `getChainId` calls — even when those handlers throw — for both the callback and no-callback constructor branches. - New test covers network-change handling via both the `onNetworkDidChange` callback and the `NetworkController:networkDidChange` messenger subscription (chain ID + eth query update on the next fetch). - All existing tests pass unchanged (53 total). 100% coverage on the changed lines; remaining uncovered lines are pre-existing. ## References Splits out the root-cause fix from the wiring PR MetaMask#9527, so that `GasFeeController` can be migrated into the shared `@metamask/wallet` init set without a dependency-ordering mechanism. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches core gas-estimation initialization and network-switch caching; behavior is intended to stay the same but timing of first network reads shifts to first fetch. > > **Overview** > **`GasFeeController` no longer touches `NetworkController` or the RPC provider during construction**, so it can be instantiated before the network stack is ready (e.g. shared `@metamask/wallet` init) without changing the public constructor or fetch semantics. > > The constructor drops eager `EthQuery` creation and immediate chain-ID resolution (`getChainId()` / `getState` + `getNetworkClientById`). Instead, **`#getEthQuery()`** and **`#getCurrentChainId()`** memoize provider and chain ID on first use inside **`_fetchGasFeeEstimateData`**. When **`onNetworkDidChange` + `getChainId`** are supplied, the callback is stored without calling `getChainId` at construct time. > > On network change, **`#onNetworkControllerDidChange`** clears the cached `ethQuery` (rebuilt on the next fetch) and still updates `currentChainId` and polling. Tests lock in zero constructor-time network/provider/`getChainId` calls and correct chain ID + provider refresh after network switches via callback or **`NetworkController:networkDidChange`**. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 66c249d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ec8eeec to
ee084b8
Compare
Wire `GasFeeController` into `@metamask/wallet`'s default initialization set as its own `InitializationConfiguration`, mirroring the `transaction-controller` / `network-controller` instances. A wired `TransactionController` delegates `GasFeeController:fetchGasFeeEstimates` lazily, so the wallet boots today but throws the first time a transaction flow needs gas estimates; wiring `GasFeeController` supplies that handler. Adds a required `instanceOptions.gasFeeController` option whose `clientId` (sent as `X-Client-Id` to the gas API) is required, so every client identifies itself; all other fields are optional and fall back to platform-agnostic defaults. wallet-cli passes `clientId: 'cli'`. The instance builds `getProvider` / `getCurrentNetworkEIP1559Compatibility` as lazy closures over `NetworkController`, and `GasFeeController` was made initialization-order-agnostic upstream (#9569), so no dependency-ordering machinery is needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ee084b8 to
baa4472
Compare
Explanation
Wires
GasFeeControllerinto@metamask/wallet's default initialization set as its ownInitializationConfigurationundersrc/initialization/instances/gas-fee-controller/, mirroring the most recently merged instances (transaction-controller,network-controller).TransactionControlleris already wired and its messenger delegatesGasFeeController:fetchGasFeeEstimates. Delegation registers a lazy handler, so the wallet boots fine today — but the first transaction flow that needs gas estimates throwsA handler for GasFeeController:fetchGasFeeEstimates has not been registered. WiringGasFeeControlleris the missing piece that lets a wiredTransactionControlleractually estimate gas.@metamask/walletis the shared controller-integration layer thatmetamask-extension,metamask-mobile, and@metamask/wallet-cliall adopt, so every value that differs between clients is an injectableinstanceOptions.gasFeeControllerslot with a platform-agnostic default — nothing is baked to one client.Constructor callbacks
GasFeeControllertakes direct callbacks rather than pure messenger delegation. The instance builds them from the wiredNetworkController:getProvider→NetworkController:getState(selectedNetworkClientId) thenNetworkController:getNetworkClientById(id).providergetCurrentNetworkEIP1559Compatibility→NetworkController:getEIP1559Compatibility(coerced to a definedboolean)onNetworkDidChange/getChainIdare omitted — the constructor already has a messenger-based network-tracking fallback when both are absent (subscribesNetworkController:networkDidChange).Injectable options + per-environment values
EIP1559APIEndpoint.../networks/<chain_id>/suggestedGasFees(dev override →gas.uat-api.cx.metamask.io)legacyAPIEndpoint${GAS_API}/networks/<chain_id>/gasPricesclientId'extension''mobile''cli'(sent asX-Client-Id); injectableinterval10_00015_00015_000; injectablegetCurrentNetworkLegacyGasAPICompatibilitychainId === BSCmainnet || BSC || POLYGON() => false; injectablegetCurrentAccountEIP1559Compatibility() => true() => true; injectableInitialization ordering
No construction-ordering machinery is needed.
GasFeeControllerwas made initialization-order-agnostic upstream (#9569): the constructor no longer eagerly resolvesNetworkController(thegetProvider/getCurrentNetworkEIP1559Compatibilitycallbacks resolve it lazily at call time), so it can be constructed beforeNetworkController.initializetherefore constructs the default set in its natural order, and thedependenciesfield /orderByDependenciessort that an earlier revision of this PR added were dropped in favor of that upstream refactor.fetchdecisionGasFeeControllerfetches gas estimates via the globalfetchinside@metamask/gas-fee-controller(it has no injectablefetchoption). This PR takes path (a): accept the globalfetch— no upstream change; works on Node 18+ (wallet-cli daemon), modern browsers, and React Native. The global-fetchusage lives inside the controller package, not inside@metamask/walletcode, and both clients already rely on this today. Adding an injectablefetchoption to@metamask/gas-fee-controlleris tracked as a possible follow-up if the convention is to be enforced strictly.Client adoption PRs
References
packages/gas-fee-controller/src/GasFeeController.tspackages/wallet/src/initialization/instances/transaction-controller/app/scripts/messenger-client-init/confirmations/gas-fee-controller-init.tsapp/core/Engine/controllers/gas-fee-controller/gas-fee-controller-init.tsRelated
GasFeeControllerinto@metamask/walletdefault initialization #9510Checklist
🤖 Generated with Claude Code
Note
Medium Risk
Breaking API for all Wallet consumers and changes the transaction gas-estimation path, but the integration follows existing controller init patterns and includes dedicated tests.
Overview
Breaking:
@metamask/walletnow bootsGasFeeControlleras part of default initialization, so wiredTransactionControllerflows can callGasFeeController:fetchGasFeeEstimatesinstead of failing with a missing handler.Consumers must pass
instanceOptions.gasFeeControllerwith a requiredclientId(sent asX-Client-Idto the gas API); other gas settings stay optional with shared defaults (production API URLs, network callbacks built fromNetworkController).@metamask/wallet-clisetsclientId: 'cli'on the daemon wallet. Extension/mobile adoption is expected in separate PRs.Reviewed by Cursor Bugbot for commit baa4472. Bugbot is set up for automated code reviews on this repo. Configure here.